在學習 Express + TypeScript + TypeORM 的過程中,TodoList API 是非常適合新手上手的練習案例。
因為它的邏輯簡單(新增、讀取、更新、刪除),卻又涵蓋了 RESTful API 的核心概念:
這樣的練習不僅可以打好基礎,還能快速理解 後端架構設計 的常見模式。
Controller(控制器)是 專門負責處理業務邏輯 的地方:
req) → 驗證/處理 → 回傳回應 (res)建立 src/controllers/todoController.ts:
import { Request, Response, NextFunction } from "express";
import { AppDataSource } from "../config/db";
import { Todo } from "../entities/Todo";
const todoRepository = AppDataSource.getRepository(Todo);
export async function getTodos(
  req: Request,
  res: Response,
  next: NextFunction
): Promise<void> {
  try {
    const todos = await todoRepository.find();
    res.json({ status: "success", data: todos });
  } catch (error) {
    next(error);
  }
}
export async function createTodo(
  req: Request,
  res: Response,
  next: NextFunction
): Promise<void> {
  try {
    const { title } = req.body;
    if (!title) {
      res.status(400).json({ status: "error", message: "Title is required" });
      return;
    }
    const newTodo = todoRepository.create({ title });
    const savedTodo = await todoRepository.save(newTodo);
    res.status(201).json({ status: "success", data: savedTodo });
  } catch (error) {
    next(error);
  }
}
export async function updateTodo(
  req: Request,
  res: Response,
  next: NextFunction
): Promise<void> {
  try {
    const { id } = req.params;
    const { title, completed } = req.body;
    const todo = await todoRepository.findOneBy({ id });
    if (!todo) {
      res.status(404).json({ status: "error", message: "Todo not found" });
      return;
    }
    todo.title = title !== undefined ? title : todo.title;
    todo.completed = completed !== undefined ? completed : todo.completed;
    const updatedTodo = await todoRepository.save(todo);
    res.json({ status: "success", data: updatedTodo });
  } catch (error) {
    next(error);
  }
}
export async function deleteTodo(
  req: Request,
  res: Response,
  next: NextFunction
): Promise<void> {
  try {
    const { id } = req.params;
    const result = await todoRepository.delete({ id });
    if (result.affected === 0) {
      res.status(404).json({ status: "error", message: "Todo not found" });
      return;
    }
    res.json({ status: "success", data: result });
  } catch (error) {
    next(error);
  }
}
Route(路由)的工作就是 決定請求要交給哪個 Controller 處理。
它就像是 導航地圖:
GET /todos → 查詢所有代辦事項 → getTodos
POST /todos → 建立新代辦 → createTodo
PUT /todos/:id → 更新某筆代辦 → updateTodo
DELETE /todos/:id → 刪除某筆代辦 → deleteTodo
這樣一來,Controller 和 Route 的責任就分得很清楚:
建立 src/routes/todoRoutes.ts:
import { Router } from "express";
import {
  getTodos,
  createTodo,
  updateTodo,
  deleteTodo,
} from "../controllers/todoController";
const router = Router();
router.get("/", getTodos);
router.post("/", createTodo);
router.put("/:id", updateTodo);
router.delete("/:id", deleteTodo);
export default router;
最後一步就是在 app.ts 裡,把 todoRoutes 整合進主程式。
這樣當使用者發送請求到 /api/todos 時,Express 就會把它交給我們剛剛寫好的 todoRoutes,再由對應的 Controller 去處理。
修改 src/app.ts:
import "reflect-metadata";
import express from "express";
import { AppDataSource } from "./config/db";
import todoRoutes from "./routes/todoRoutes";
import dotenv from "dotenv";
dotenv.config();
const app = express();
app.use(express.json());
app.use("/api/todos", todoRoutes); // 加上 Todo 路由
app.get("/", (req, res) => {
  res.send("Hello, iThome 2025!");
});
const PORT = process.env.PORT || 3000;
AppDataSource.initialize()
  .then(() => {
    console.log("📦 DB Connected!");
    app.listen(PORT, () => {
      console.log(`🚀 Server running on http://localhost:${PORT}`);
    });
  })
  .catch((err) => {
    console.error("❌ DB connection failed:", err);
  });
👉 到這裡,我們就完成了一個最基礎的 TodoList API。
但是目前還沒有資料庫可以測試 (還不能用 Postman 打 API 😂),下一篇我們就來介紹 Render 服務上的資料庫應用,把整段串接起來。
commit : Day 8 initialize todo‑list API